//! Container for sparse ddata relating to specific blocks in the world,
//! organized by chunk.

use std::collections::BTreeMap;

use craftdig_math::{BlkCoord, ChunkCoord, SubchunkPos};

use super::ChunkSlotmap;

#[derive(Clone, Debug)]
pub struct BlockMap<V> {
    chunks: BTreeMap<ChunkCoord, ChunkMap<V>>,
}

impl<V> BlockMap<V> {
    pub fn new() -> Self {
        Self {
            chunks: BTreeMap::new(),
        }
    }

    /// Gets a ref to a chunk.
    pub fn get_chunk(&self, cc: ChunkCoord) -> Option<&ChunkMap<V>> {
        self.chunks.get(&cc)
    }

    /// Returns a mut ref to a chunk map.  If the chunk map ends up being being
    /// made empty as a result of this operation, it won't be automatically
    /// cleaned up.
    pub fn get_chunk_mut(&mut self, cc: ChunkCoord) -> Option<&mut ChunkMap<V>> {
        self.chunks.get_mut(&cc)
    }

    /// Gets a mut ref to a chunk, creating one if necessary.
    pub fn get_or_create_chunk_mut(&mut self, cc: ChunkCoord) -> &mut ChunkMap<V> {
        self.chunks.entry(cc).or_insert_with(ChunkMap::new)
    }

    /// Inserts an entry for a block, creating a chunk entry if needed.
    ///
    /// Returns the old value if there was one.
    pub fn insert(&mut self, pos: BlkCoord, val: V) -> Option<V> {
        let (cc, scp) = pos.split();
        let chunk = self.get_or_create_chunk_mut(cc);
        chunk.insert(scp, val)
    }

    pub fn get(&self, pos: BlkCoord) -> Option<&V> {
        let (cc, scp) = pos.split();
        let chunk = self.get_chunk(cc)?;
        chunk.get(scp)
    }

    pub fn get_mut(&mut self, pos: BlkCoord) -> Option<&mut V> {
        let (cc, scp) = pos.split();
        let chunk = self.get_chunk_mut(cc)?;
        chunk.get_mut(scp)
    }

    /// Removes the entry for a block.  If this removes the last entry from a
    /// chunk, also removes the chunk.
    pub fn remove(&mut self, pos: BlkCoord) -> Option<V> {
        let (cc, scp) = pos.split();
        let chunk = self.get_chunk_mut(cc)?;
        let v = chunk.remove(scp);

        // If the chunk is made empty, we want to remove it from our table too.
        if chunk.is_empty() {
            self.clear_chunk(cc);
        }

        v
    }

    /// Clears all entries.
    pub fn clear(&mut self) {
        self.chunks.clear();
    }

    /// Clears entries in a chunk, if present.
    pub fn clear_chunk(&mut self, cc: ChunkCoord) {
        if let Some(chunk) = self.get_chunk_mut(cc) {
            chunk.clear();
        }
    }

    /// Clears chunks that have no entries in them, such as after calls to
    /// `.get_chunk_mut` that left empty chunks.
    pub fn clear_empty_chunks(&mut self) -> Vec<ChunkCoord> {
        let empties = self
            .chunks
            .iter()
            .filter(|(_, c)| c.is_empty())
            .map(|(cc, _)| *cc)
            .collect::<Vec<_>>();

        empties.iter().for_each(|cc| self.clear_chunk(*cc));
        empties
    }

    /// Returns the number of chunks that we have entries in.
    pub fn num_chunks(&self) -> usize {
        self.chunks.len()
    }

    /// Returns the total number of blocks that we have entries for, across all
    /// chunks we're tracking.  This *could* be less than the number of chunks,
    /// if there are dangling empty chunks.
    pub fn num_block_entries(&self) -> usize {
        self.chunks.values().map(|c| c.num_entries()).sum()
    }

    /// Iterates over the entries in the blockmap.
    pub fn iter_entries(&self) -> impl Iterator<Item = (BlkCoord, &V)> {
        self.chunks.iter().flat_map(|(cc, cmap)| {
            cmap.iter_entries()
                .map(|(scp, v)| (BlkCoord::from_parts(*cc, scp), v))
        })
    }
}

impl<V> Default for BlockMap<V> {
    fn default() -> Self {
        Self::new()
    }
}

/// Sparse table of entries for a specific chunk.
#[derive(Clone, Debug)]
pub struct ChunkMap<V> {
    blocks: BTreeMap<SubchunkPos, V>,
}

impl<V> ChunkMap<V> {
    pub fn new() -> Self {
        Self {
            blocks: BTreeMap::new(),
        }
    }

    pub fn insert(&mut self, pos: SubchunkPos, val: V) -> Option<V> {
        self.blocks.insert(pos, val)
    }

    pub fn get(&self, pos: SubchunkPos) -> Option<&V> {
        self.blocks.get(&pos)
    }

    pub fn get_mut(&mut self, pos: SubchunkPos) -> Option<&mut V> {
        self.blocks.get_mut(&pos)
    }

    pub fn remove(&mut self, pos: SubchunkPos) -> Option<V> {
        self.blocks.remove(&pos)
    }

    pub fn clear(&mut self) {
        self.blocks.clear();
    }

    pub fn num_entries(&self) -> usize {
        self.blocks.len()
    }

    pub fn is_empty(&self) -> bool {
        self.blocks.is_empty()
    }

    /// Gets
    pub fn get_or_create_with(&mut self, pos: SubchunkPos, f: impl Fn() -> V) -> &mut V {
        self.blocks.entry(pos).or_insert_with(f)
    }

    /// Iterates over the entries in this chunkmap.
    pub fn iter_entries(&self) -> impl Iterator<Item = (SubchunkPos, &V)> {
        self.blocks.iter().map(|(scp, v)| (*scp, v))
    }
}

impl<V> Default for ChunkMap<V> {
    fn default() -> Self {
        Self::new()
    }
}

impl<V: Clone + Default> ChunkMap<V> {
    /// Converts the chunk map into a chunk slotmap by cloning the entries.
    ///
    /// The slotmap form has a flatter representation which makes for faster
    /// lookups.
    pub fn to_slotmap(&self) -> ChunkSlotmap<V> {
        ChunkSlotmap::from_fn(|scp| self.get(scp).cloned().unwrap_or_else(V::default))
    }
}
